package com.abso.sunlight.explorer.wizards;

import java.awt.Color;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.osgi.framework.Bundle;

import com.abso.sunlight.api.Legislator;
import com.abso.sunlight.api.SunlightException;
import com.abso.sunlight.explorer.CongressExplorerPlugin;
import com.lowagie.text.Anchor;
import com.lowagie.text.BadElementException;
import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfDestination;
import com.lowagie.text.pdf.PdfOutline;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;

/**
 * The PDF Exporter is able to export a list of legislators into a PDF document.
 */
public class PDFExporter extends PdfPageEventHelper {

    /** The blue AWT color used to create a specific medium font. */
    private static final Color BLUE_AWT_COLOR = new Color(JFaceResources.getColorRegistry().get(JFacePreferences.COUNTER_COLOR)
            .getRed(), JFaceResources.getColorRegistry().get(JFacePreferences.COUNTER_COLOR).getGreen(), JFaceResources
            .getColorRegistry().get(JFacePreferences.COUNTER_COLOR).getBlue());

    /** The blue font with medium size. */
    private static final Font BLUE_MEDIUM_FONT = FontFactory.getFont(FontFactory.HELVETICA, 15, Font.BOLD, BLUE_AWT_COLOR);

    /** The black font with big size. */
    private static final Font BIG_FONT = FontFactory.getFont(FontFactory.HELVETICA, 20, Font.BOLD);

    /** The black font with normal size. */
    private static final Font NORMAL_FONT = FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD);

    /** The black font with size suitable for printing emails. */
    private static final Font EMAIL_FONT = FontFactory.getFont(FontFactory.HELVETICA, 12, Font.NORMAL);

    /** The blue font with normal size. */
    private static final Font BLUE_NORMAL_FONT = FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, BLUE_AWT_COLOR);

    /** The list of legislators to export. */
    private List<Legislator> legislators;

    /** The destination file. */
    private File file;

    /** The PDF document being built. */
    private Document doc;

    /** The font used in the footer to print the page number. */
    private BaseFont footerFont;

    /** The PDF template. */
    private PdfTemplate tpl;

    /** The output PDF writer. */
    private PdfWriter pdfWriter;

    /** The opened zip file containing photos. */
    private ZipFile photosZipFile;

    /** Indicates if photos must be included in the PDF. */
    private boolean includePhotos;

    /**
     * Constructs a new PDF Exporter.
     * 
     * @param legislators
     *            the list of legislators to export.
     * @param file
     *            the destination file.
     * @param includePhotos
     *            indicates if photos must be included in the PDF.
     */
    public PDFExporter(List<Legislator> legislators, File file, boolean includePhotos) {
        this.legislators = legislators;
        this.file = file;
        this.includePhotos = includePhotos;
    }

    /**
     * Performs the export.
     * 
     * @param monitor
     *            the progress monitor.
     * @throws SunlightException
     *             if an exception occurred performing the export operation.
     */
    public void export(IProgressMonitor monitor) throws SunlightException {
        OutputStream outStream = null;
        try {
            monitor.beginTask("Exporting", legislators.size() + 3);

            /* creates the PDF document */
            doc = new Document(PageSize.LETTER);
            outStream = new FileOutputStream(file);
            pdfWriter = PdfWriter.getInstance(doc, outStream);
            pdfWriter.setViewerPreferences(PdfWriter.PageModeUseOutlines);
            pdfWriter.setPageEvent(this);
            doc.open();
            footerFont = BaseFont.createFont(FontFactory.HELVETICA, BaseFont.WINANSI, false);
            tpl = pdfWriter.getDirectContent().createTemplate(100, 100);
            tpl.setBoundingBox(new Rectangle(-20, -20, 100, 100));
            monitor.worked(1);

            /* sets metadata */
            writeMetadata();
            monitor.worked(1);

            /* adds the cover page */
            writeCover();
            monitor.worked(1);

            /* prepares the photos zip */
            if (includePhotos) {
                Bundle bundle = Platform.getBundle(CongressExplorerPlugin.PLUGIN_ID);
                URL photosZipURL = bundle.getEntry("images/100x125.zip");
                File file = new File(FileLocator.toFileURL(photosZipURL).getFile());
                if (file.exists()) {
                    photosZipFile = new ZipFile(file);
                }
            }

            /* writes legislators */
            for (int i = 0; i < legislators.size(); i++) {
                writePage(legislators.get(i));
                if (monitor.isCanceled()) {
                    return;
                }
                monitor.worked(1);
            }
        } catch (IOException e) {
            throw new SunlightException(e);
        } catch (DocumentException e) {
            throw new SunlightException(e);
        } finally {
            if (doc != null) {
                try {
                    doc.close();
                } catch (Throwable e) {
                }
            }
            if (photosZipFile != null) {
                try {
                    photosZipFile.close();
                } catch (IOException e) {
                }
            }
            monitor.worked(1);
            monitor.done();
        }
    }

    /** Writes PDF metadata. */
    private void writeMetadata() {
        doc.addTitle("Congress Explorer - Exported Data");
        doc.addAuthor("ABSo (http://abso.freehostia.com)");
        doc.addSubject("This document contains information about legislators.");
        doc.addKeywords("Congress, Legislator, Sunlight");
        doc.addCreator("Congress Explorer using iText");
    }

    /**
     * Writes the cover page.
     * 
     * @throws BadElementException
     *             if an exception occurred creating the cover.
     * @throws MalformedURLException
     *             if an exception occurred creating the cover.
     * @throws IOException
     *             if an exception occurred creating the cover.
     * @throws DocumentException
     *             if an exception occurred creating the cover.
     */
    private void writeCover() throws BadElementException, MalformedURLException, IOException, DocumentException {
        InputStream coverImgInStream = null;
        try {
            doc.add(new Paragraph(" "));
            Paragraph coverPara = new Paragraph();
            coverPara.setSpacingBefore(150);
            coverPara.setSpacingAfter(50);
            coverImgInStream = this.getClass().getResourceAsStream("cover.png");
            Image bannerImage = Image.getInstance(IOUtils.toByteArray(coverImgInStream));
            bannerImage.setAlignment(Image.MIDDLE);
            coverPara.add(bannerImage);
            doc.add(coverPara);
            doc.newPage();
        } finally {
            IOUtils.closeQuietly(coverImgInStream);
        }
    }

    /**
     * Writes into a page the information about a specific legislator.
     * 
     * @param legislator
     *            the legislator whose information must be printed in the page.
     * @throws DocumentException
     *             if an exception occurred writing the page.
     */
    private void writePage(Legislator legislator) throws DocumentException {

        /* adds the outline */
        PdfContentByte cb = pdfWriter.getDirectContent();
        PdfDestination destination = new PdfDestination(PdfDestination.FITH);
        new PdfOutline(cb.getRootOutline(), destination, getOutlineName(legislator));

        /* writes the title and name */
        doc.add(new Paragraph(legislator.getTitle().toString(), BLUE_MEDIUM_FONT));

        /* writes the name */
        doc.add(new Paragraph(getName(legislator), BIG_FONT));

        /* writes the photo */
        if (includePhotos) {
            if (!StringUtils.isBlank(legislator.getBioguideId())) {
                try {
                    Image image = null;
                    if (photosZipFile != null) {
                        ZipEntry zipEntry = photosZipFile.getEntry("100x125/" + legislator.getBioguideId() + ".jpg");
                        if (zipEntry != null) {
                            InputStream inStream = null;
                            try {
                                inStream = photosZipFile.getInputStream(zipEntry);
                                image = Image.getInstance(IOUtils.toByteArray(inStream));
                            } catch (IOException e) {
                            } finally {
                                IOUtils.closeQuietly(inStream);
                            }
                        }
                    }
                    if (image == null) {
                        image = Image.getInstance(new URL("http://bioguide.congress.gov/bioguide/photo/"
                                + legislator.getBioguideId().charAt(0) + "/" + legislator.getBioguideId() + ".jpg"));
                    }
                    if (image != null) {
                        image.scaleToFit(100, 125);
                        doc.add(image);
                    }
                } catch (MalformedURLException e) {
                } catch (IOException e) {
                } catch (BadElementException e) {
                }
            }
        }

        /* writes other information */
        addStringProperty("Party", legislator.getParty().toString());
        addStringProperty("State", legislator.getState().toString());
        addStringProperty("District", legislator.getDistrict().toString());
        addStringProperty("In Office", legislator.isInOffice() ? "Active" : "Inactive");
        addStringProperty("Gender", legislator.getGender().toString());
        addStringProperty("Office Phone", legislator.getPhone().toString());
        addStringProperty("Office Fax", legislator.getFax().toString());
        addURLProperty("Web Site", legislator.getWebSite());
        addURLProperty("Web Contact Form", legislator.getWebContactForm());
        addURLProperty("Email", legislator.getEmail(), true);
        addStringProperty("Office Address", legislator.getCongressOffice());
        addStringProperty("Bioguide ID", legislator.getBioguideId());
        addURLProperty("", getURL("http://bioguide.congress.gov/scripts/biodisplay.pl?index=${id}", legislator
                .getBioguideId()));
        addStringProperty("Vote Smart ID", legislator.getVoteSmartId());
        addURLProperty("", getURL("http://votesmart.org/bio.php?can_id=${id}", legislator.getVoteSmartId()));
        addStringProperty("FEC ID", legislator.getFecId());
        addURLProperty("", getURL("http://query.nictusa.com/cgi-bin/can_detail/${id}", legislator.getFecId()));
        addStringProperty("GovTrack.us ID", legislator.getGovTrackId());
        addURLProperty("", getURL("http://www.govtrack.us/congress/person.xpd?id=${id}", legislator.getGovTrackId()));
        addStringProperty("CRP ID", legislator.getCrpId());
        addURLProperty("", getURL("http://www.opensecrets.org/politicians/summary.php?cid=${id}", legislator.getCrpId()));
        addStringProperty("Eventful.com ID", legislator.getEventfulId());
        addURLProperty("", getURL("http://eventful.com/performers/${id}", legislator.getEventfulId()));
        addStringProperty("Twitter ID", legislator.getTwitterId());
        addURLProperty("", getURL("http://twitter.com/${id}", legislator.getTwitterId()));
        addURLProperty("Congresspedia URL", legislator.getCongresspediaURL());
        addURLProperty("YouTube URL", legislator.getYouTubeURL());

        /* forces a page break */
        doc.newPage();
    }

    /**
     * Returns the legislator's name, composed using first, middle, last, suffix an nickname.
     * 
     * @param legislator
     *            the legislator whose composite name must be returned.
     * @return the legislator's name.
     */
    private String getName(Legislator legislator) {
        List<String> nameTokens = new ArrayList<String>(5);
        nameTokens.add(legislator.getFirstName());
        nameTokens.add(legislator.getMiddleName());
        if (!StringUtils.isBlank(legislator.getNickname())) {
            nameTokens.add("\"" + legislator.getNickname() + "\"");
        }
        nameTokens.add(legislator.getLastName());
        nameTokens.add(legislator.getNameSuffix());
        String name = StringUtils.join(nameTokens, " ");
        return name;
    }

    /**
     * Returns the legislator's name suitable to be included in the PDF outline.
     * 
     * @param legislator
     *            the legislator's name.
     * @return the legislator's name.
     */
    private String getOutlineName(Legislator legislator) {
        List<String> nameTokens = new ArrayList<String>(5);
        nameTokens.add(legislator.getLastName());
        if (!StringUtils.isBlank(legislator.getNameSuffix())) {
            nameTokens.add(legislator.getNameSuffix());
        }
        String quotedNickname = "";
        if (!StringUtils.isBlank(legislator.getNickname())) {
            quotedNickname = "\"" + legislator.getNickname() + "\"";
        }
        nameTokens.add(StringUtils.trim(legislator.getFirstName() + " " + legislator.getMiddleName() + " " + quotedNickname));
        String name = StringUtils.join(nameTokens, ", ");
        return name;
    }

    /**
     * Adds a string property into the document, as a paragraph.
     * 
     * @param legislator
     *            the legislator whose property must be printed.
     * @param name
     *            the name of the property.
     * @param value
     *            the value of the property.
     * @throws DocumentException
     *             if an exception occurred adding the property.
     */
    private void addStringProperty(String name, String value) throws DocumentException {
        if (!StringUtils.isBlank(value)) {
            Paragraph para = new Paragraph();
            para.setSpacingBefore(1);
            Chunk nameChunk = new Chunk(name, BLUE_NORMAL_FONT);
            para.add(nameChunk);
            Chunk spaceChunk = new Chunk("    ", NORMAL_FONT);
            para.add(spaceChunk);
            Chunk valueChunk = new Chunk(value, NORMAL_FONT);
            para.add(valueChunk);
            doc.add(para);
        }
    }

    /**
     * Adds a URL property into the document, as a paragraph.
     * 
     * @param legislator
     *            the legislator whose property must be printed.
     * @param name
     *            the name of the property.
     * @param value
     *            the value of the property.
     * @throws DocumentException
     *             if an exception occurred adding the property.
     */
    private void addURLProperty(String name, String value) throws DocumentException {
        addURLProperty(name, value, false);
    }

    /**
     * Adds a string property into the document, as a paragraph.
     * 
     * @param legislator
     *            the legislator whose property must be printed.
     * @param name
     *            the name of the property.
     * @param value
     *            the value of the property.
     * @param email
     *            if the URL represents a mail address.
     * @throws DocumentException
     *             if an exception occurred adding the property.
     */
    private void addURLProperty(String name, String value, boolean email) throws DocumentException {
        if (!StringUtils.isBlank(value)) {
            Paragraph para = new Paragraph();
            para.setSpacingBefore(1);
            Chunk nameChunk = new Chunk(name, BLUE_NORMAL_FONT);
            para.add(nameChunk);
            Chunk spaceChunk = new Chunk("    ", NORMAL_FONT);
            para.add(spaceChunk);
            if (email) {
                Chunk emailChunk = new Chunk(value, EMAIL_FONT);
                para.add(emailChunk);
            } else {
                Anchor valueAnchor = new Anchor(value);
                valueAnchor.setReference(value);
                para.add(valueAnchor);
            }
            doc.add(para);
        }
    }

    /**
     * Returns a URL replacing a predefined ${id} token with the <i>id</i> argument.
     * 
     * @param urlTemplate
     *            the URL template, including a ${id} token.
     * @param id
     *            the identifier's value.
     * @return the final URL.
     */
    private String getURL(String urlTemplate, String id) {
        if (!StringUtils.isBlank(id)) {
            return StringUtils.replace(urlTemplate, "${id}", id);
        } else {
            return "";
        }
    }

    @Override
    public void onEndPage(PdfWriter writer, Document document) {
        if (writer.getPageNumber() >= 2) {
            PdfContentByte cb = writer.getDirectContent();
            cb.saveState();
            String text = "Page " + writer.getPageNumber() + " of ";
            float textSize = footerFont.getWidthPoint(text, 12);
            float textBase = document.bottom() - 20;
            cb.beginText();
            cb.setFontAndSize(footerFont, 12);
            float adjust = footerFont.getWidthPoint("0", 12);
            cb.setTextMatrix(document.right() - textSize - adjust, textBase);
            cb.showText(text);
            cb.endText();
            cb.addTemplate(tpl, document.right() - adjust, textBase);
            cb.restoreState();
        }
    }

    @Override
    public void onCloseDocument(PdfWriter writer, Document document) {
        tpl.beginText();
        tpl.setFontAndSize(footerFont, 12);
        tpl.setTextMatrix(0, 0);
        tpl.showText(Integer.toString(writer.getPageNumber() - 1));
        tpl.endText();
    }

}
